{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 问题：谈下 Python 的 GIL\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "面试官想从这道题中考察面试人什么？\n",
    "面试官想知道你对 GIL 的认识，为什么它会存在？对多线程程序的执行性能有什么影响？以及如何解决？了解这些对优化 Python 性能会有很大帮助。\n",
    "\n",
    "答案\n",
    "全局解释器锁 GIL 是解释器本身的单个锁，执行任何 Python 字节码都需要获取解释器锁，它确保任何时候都只有一个 Python 线程执行。但 GIL 最大的问题就是 Python 的多线程程序并不能利用多核 CPU 的优势。\n",
    "\n",
    "拓展\n",
    "尽管 Python 完全支持多线程编程，但是解释器的 C 语言实现部分在完全并行执行时并不是线程安全的。这是因为解释器被 GIL 锁保护着。GIL，全称是全局解释器锁 Global interpreter lock，它确保任何时候都只有一个 Python 线程执行。\n",
    "\n",
    "Tips : 线程安全是在多线程的环境下，能够保证多个线程同时执行时程序依旧运行正确。\n",
    "\n",
    "为什么会有这个全局解释器锁？\n",
    "我们先想像一下，如果同时有两个线程，一个我们用 A 表示，另一个用 B 表示。它俩都对保存了对内存中同一个对象 C 的引用。这个时候，A 想要释放对这个对象的引用，在之前讲 Python 内存机制中提到过垃圾回收，当一个对象的引用计数减少至 0 的时候这段内存会被回收。那么，A 想要释放这个对象，有两个步骤：1，将该对象的引用计数减 1；2，去检查该对象的引用计数是否为 0，是否需要释放内存。A 这个时候刚好完成步骤 1，被线程调度机制挂起了，而 B 被唤醒了，这个时候 B 也开始销毁对象 C，刚好蹭 A 挂起的时候，B 完成了销毁 C 的动作，A 再回头去完成步骤 2 的时候，已经没有东西可以释放了。所以，Python 解释器被一个全局解释器锁保护着，实现不同线程对同一共享资源访问的互斥，这样可以保持同一个引用计数变量的安全性，从而不会对它们进行不一致的修改。另一方面，当一个线程获得了解释器的访问权，它就可以对 Python 对象进行操作或调用 CPython API，其它线程必须等那个线程释放解释器的访问权。简单地说就是每一个解释器进程，只能同时仅有一个线程来执行，获得相关的锁，存取相关的资源。\n",
    "\n",
    "Tips : GIL 只局限于 CPython，与 Python 语言无关，其它的 Jython，IronPython 没有这种限制。CPython 是标准的 Python 解释器。CPython 执行 Python 程序有两个步骤：1. 将文本源码编译成字节码（.pyc 文件）；2. 在解释器上运行字节码。GIL 是解释器本身的单个锁，执行任何 Python 字节码都需要获取解释器锁。这可以防止死锁（因为只有一个锁）并且不会引入太多的性能开销。\n",
    "\n",
    "但是，GIL 会带来的问题有，多个线程由于 GIL 锁的存在无法利用多核 CPU ，比如，一个使用了多个线程的计算密集型程序只会在一个单 CPU 上面运行，换言之，Python 使得多处理器退化成单处理器。所以，GIL 只会影响到那些严重依赖 CPU 的程序。\n",
    "\n",
    "计算密集型\n",
    "也称 CPU 密集型。计算密集型任务的特点是要进行大量的计算，消耗 CPU 资源，比如计算圆周率、对视频进行高清解码等等，全靠 CPU 的运算能力。这种计算密集型任务虽然也可以用多任务完成，但是任务越多，花在任务切换的时间就越多，CPU 执行任务的效率就越低，所以，要最高效地利用 CPU，计算密集型任务同时进行的数量应当等于 CPU 的核心数。对于计算密集型任务，最好用 C 语言编写。\n",
    "\n",
    "IO 密集型\n",
    "涉及到网络、磁盘 IO 的任务都是 IO 密集型任务，这类任务的特点是 CPU 消耗很少，任务的大部分时间都在等待 IO 操作完成，因为 IO 的速度远远低于 CPU 和内存的速度。对于 IO 密集型任务，任务越多，CPU 效率越高，但也有一个限度。常见的大部分任务都是 IO 密集型任务，比如 Web 应用。对于 IO 密集型任务，最合适的语言就是开发效率最高（代码量最少）的语言，脚本语言是首选，C 语言最差。 在 Python 中，尽管 GIL 一次只允许一个线程执行 Python 字节码。但是，标准库中使用 C 语言编写的 I/O 函数都会释放 GIL，也就是说，I/O 操作在等待操作系统返回结果时会释放 GIL，Python 可以调度切换其它线程，这样可以实现多线程并发，执行效率会大大提高。 可能还是有很多人不能理解，明明每次只能有一个线程，为什么还说多线程并发呢？这里先说一下，并发是指多个任务可以同时存在，但不是同时执行。关于并发，在下一个问题会详述。\n",
    "\n",
    "所以，Python 的多线程在多核 CPU 上，只对于 IO 密集型计算产生正面效果；而当有至少有一个 CPU 密集型线程存在，那么多线程效率会由于 GIL 而大幅下降。\n",
    "\n",
    "多核多线程 vs 单核多线程\n",
    "多核多线程比单核多线程更差，原因是单核下多线程，每次释放 GIL，唤醒的那个线程都能获取到 GIL 锁，所以能够无缝执行，但多核下，CPU0 释放 GIL 后，其他 CPU 上的线程都会进行竞争，但 GIL 可能会马上又被 CPU0 拿到，导致其他几个 CPU 上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态，这样会造成线程颠簸（thrashing），导致效率更低。另外，线程数的选择也决定了性能的快慢，并不是越多越快，因为添加更多线程的收益递减 - 添加更多线程以加速 IO 很有帮助，但是这样做只会减慢您执行线程所做的所有其他事情，因为只有一个线程可以运行一次。当有多个线程同时运行一个线程时，操作系统必须进行大量上下文切换才能为每个线程分配 CPU 时间，这比仅使用较少的线程要慢。\n",
    "\n",
    "如何解决 GIL 对多线程计算密集型程序的执行性能问题\n",
    "我们了解了 GIL 的特点，可以根据你的实际情况去解决此类问题：\n",
    "\n",
    "如果必须完全工作在 Python 环境中，可以将基于线程的方案转换成基于进程的方案。这需要利用 multiprocessing 模块或 Concurrent.futures 模块 ProcessPoolExecutor 类创建一个进程池。进程池会在另外一个进程中启动一个单独的 Python 解释器来工作。由于计算任务在一个隔离的进程中，由单独的解释器中执行，那么就不会受限于 GIL 了；\n",
    "\n",
    "Tips : 关于进程和线程的区别可以参考下一个问题。\n",
    "\n",
    "如果不需要完全工作在 Python 环境中，可以使用 C 扩展编程技术。这样可以将计算密集型任务转移给 C，跟 Python 独立，在工作的时候在 C 代码中释放 GIL；\n",
    "\n",
    "使用别的 Python 解释器。像 JPython 和 IronPython 这样的解析器就没有 GIL 的烦恼。\n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
